Introduzione all'assembly
Gennaio 2000 - Cosma Colanicchia
cloud.cc@tiscalinet.it
Ottimizzato per la risoluzione di 1024x768 pixel


l'assembly
  la struttura
  le variabili
  le istruzioni di trasferimento
  le istruzioni aritmetiche
  strutture di controllo
  l'input - output tramite l'INT 21h
  la pila o stack
  le procedure assembly
  interfacciamento tra assembly e C
 

programmare in assembly tutto quello di cui abbiamo bisogno per programmare in assembly è un editor di testo puro (l'EDIT del dos va benissimo) e un compilatore assembly (molto usato in campo didattico il Turbo Assembler della Borland, ma credo se ne trovino molti validi anche freeware). Tutto quello che dobbiamo fare è scrivere il codice, salvarlo con estensione .ASM, e poi lanciare l'assembler. Questo in genere non genera direttamente un file eseguibile, ma un file detto programma oggetto con estensione .OBJ. Per ottenere l'eseguibile vero e proprio bisogna fare il cosidetto linking con un secondo programma (di solito distribuito insieme al compilatore) detto linker. Dopo questo passaggio avremo, finalmente, un file .EXE oppure .COM.
Entrambi i file .EXE e .COM sono eseguibili, ma hanno una struttura leggermente differente. Molto spesso dobbiamo scrivere programmi molto piccoli, tanto che sia il codice sia i dati entrano tutti in un solo segmento (64kbyte). In questo caso possiamo generare un file .COM, più compatto di un .EXE, in cui i valori di tutti registri segmento coincidono. Questo programma occuperà meno spazio in memoria in fase di esecuzione.

In assembly, più che in ogni altro linguaggio, è fondamentale pianificare ogni cosa prima di inziare a scrivere il codice, visto che è molto facile commettere errori, e trovare un bug in un listato ASM spesso non è affatto semplice. (date un'occhiata all'appendice sui diagrammi di flusso)
Andiamo a vedere come è fatta la struttura di un listato assembly

Abbiamo già visto l'utilizzo dei segment di memoria. Nel programma assembly dobbiamo specificare al PC che segmenti intendiamo utilizzare per il codice, per i dati e per lo stack (si tratta di inserire dei valori nei registri segmento: Code Segment, Data Segment, Stack Segment e Extra Segment). In pratica:

IMPORTANTE: Il codice riportate negli esempi si riferisce al compilatore Turbo Assembler della Borland. Nel caso si usi un compilatore differente forse saranno necessarie delle piccole modifiche nella struttura del programma.

;nome del programma
;funzioni del programma

Dati SEGMENT
Dati ENDS

Sistema SEGMENT STACK
Sistema ENDS

Codice SEGMENT
Codice ENDS

END <etichetta di partenza>
 

Nel programma qui sopra non facciamo altro che inizializzare i vari segmenti tramite le parole SEGMENT e ENDS (che sta per ENDSegment), che rispettivamente aprono e chiudono il segmento. La parola END serve a chiudere il programma, e anche ad indicare da quale istruzione partira il programma (gli associamo a tale scopo un'etichetta). Poi dobbiamo inserire, come abbiamo detto, i valori dei vari segmenti nei registri segmento del processore. Vediamo come cambia il programma...
 

;nome del programma
;funzioni del programma

Dati SEGMENT
Dati ENDS

Sistema SEGMENT STACK
Sistema ENDS

Codice SEGMENT
        ASSUME CS:Codice, DS:Dati, SS:Sistema, ES:Dati
        inizio: <...>
                <...>
Codice ENDS

END inizio

Abbiamo aggiunto la parola chiave ASSUME e l'etichetta inizio per la prima istruzione del programma, nel segmento Codice. Un'etichetta non è altro che una specie di segnalibro, a cui possiamo fare riferimento quando dobbiamo effettuare dei salti nell'esecuzione del codice. Ne abbiama usata una, 'inizio', in corrispondenza della prima istruzione del programma, e l'abbiamo anche specificata nell'istruzione END.
A questo punto la struttura base del programma è realizzata. Nel segmento Codice possiamo inserire le istruzioni e nel segmento Dati possiamo dichiarare le variabili. Ma come?

le variabili andranno a trovare posto nel segmento di memoria puntato dal registro Data Segment. Basta dichiararle all'interno del segmento che abbiamo associato a tale registro (nel nostro caso il segmento Dati).
Possiama dichiarare soltanto due tipi di variabili in assembly: BYTE e WORD. L'unica differenza tra i due è la lunghezza: un byte per il primo e due per il secondo (con la parola WORD infatti si intendono in genere due byte). Per la dichiarazione si usano rispettivamente le parole DB e DW (Dichiara Byte e Dichiara Word). Inseriamo due variabili nel nostro programma:
 

;nome del programma
;funzioni del programma

Dati SEGMENT
        Num1 DB 00h
        Num2 DW 00h
Dati ENDS

Sistema SEGMENT STACK
Sistema ENDS

Codice SEGMENT
        ASSUME CS:Codice, DS:Dati, SS:Sistema, ES:Dati
        inizio: <...>
                <...>
Codice ENDS

END inizio
 

Abbiamo aggiunto due righe nel segmento Dati. La prima dichiara la variabile Num1 di tipo byte, e la inizializza al valore 00h (la 'h' sta ad indicare che è un valore esadecimale. Possiamo specificare ogni costante numerica anche come decimale e binaria mettendo al posto di 'h' le lettere 'd' o 'b'). La seconda riga è analoga alla prima, ma Num2 viene dichiarata di tipo word.
Una struttura dati molto utilizzata nella programmazione è il vettore, cioè una struttura lineare composta da un numero predefinito di byte disposti in memoria uno dopo l'altro, a cui viene associato un solo identificativo.
Per dichiarare un vettore in assembly esistono vari modi:
 
 

Vett1 DB 00h, 00h, 00h...  Dichiara un vettore di byte di nome Vett1, di lughezza pari al numero di valori di inizializzazione specificati
Vett2 DW 4 DUP 00h  Dichiara un vettore di word di nome Vett2, di lunghezza 4 e tutti i valori inizializzati a 00h
Vett3 DB 3 DUP (?)  Dichiara un vettore di byte di nome Vett3, di lunghezza 3 senza inizializzare gli elementi

Per ora non proccupiamoci oltre dei vettori: sappiano che esistono e come si dichiarano, diventando più esperti imparerete senz'altro anche ad usarli.
Prima di passare oltre, vediamo la direttiva EQU. Serve a definire una costante: in pratica associa un valore ad una certa parola. In fase di compilazione (cioè mentre il compilatore prende il vostro file sorgente e lo trasforma, se tutto va bene, in uno eseguibile) ogni costante viene semplicemente sostituita dal suo valore. Il vantaggio di usare delle costanti sta tutto nella leggibilità del programma: il listato sarà molto più comprensibile ed eventuali modifiche più semplici e veloci.

Op1 EQU 10d Associa alla stringa Op1 il valore decimale 10

istruzioni di trasferimento sono tra le operazioni fondamentali per un programma assembly. Servono a trasferire dati ed indirizzi dalla memoria ai registri e viceversa.
Per trasferire dati a 8 o 16 bit si usa l'istruzione MOV, il cui formato è:
MOV destinazione, sorgente
dove:
sorgentepuò essere una costante o un riferimento (identificatore) ad un registro o una variabile
destinazione può essere solo un riferimento (identificatore) ad un registro o a una variabile
inoltre destinazione e sorgente non possono essere entrambi due variabili e devono essere compatibili in termini di dimensioni
Ad esempio MOV AX, BX copia il contenuto del registro BX in AX.
MOV AX, 0000h mette il valore 00 esadecimale nel registro AX.
MOV AL, 01h mette il valore 01 esadecimale nel byte basso (AL) di AX.
Quando trasferiamo dati dalla memoria, dobbiamo indicare all'assemblatore il numero dei byte (1 o 2) a cui stiamo facendo rifermento. Possiamo farlo tramite le direttive BYTE PTR e WORD PTR (byte pointer e word pointer):
MOV BX, WORD PTR num1
MOV BH, BYTE PTR num2
Per risalire all'indirizzo di una variabile in memoria, usiamo l'istruzione LEA (Load Effective Address):
LEA destinazione, sorgente
Pone in destinazione (un registro a 16 bit) l'indirizzo di riferimento della variabile sorgente: LEA AX, num1

istruzioni logiche servono ad eseguire operazioni di tipo logico (not, and...) sui dati.
OR destinazione, sorgente pone in destinazione il risultato del OR inclusivo tra destinazione e sorgente
AND destinazione, sorgente pone in destinazione il risultato dell'AND logico tra destinazione e sorgente
XOR destinazione, sorgente pone in destinazione il risultato dell'OR esclusivo tra destinazione e sorgente
NOT destinazione pone in destinazione il risultato del NOT logico di destinazione
dove:
destinazione può essere un registro o una variabile
sorgente può essere un registro, una variabile o una costante
destinazione e sorgente devono essere compatibili in termini di dimensioni

tabella di verità del OR

A B OR
0 0 0
0 1 1
1 0 1
1 1 1

tabella di verità dell'AND

A B AND
0 0 0
0 1 0
1 0 0
1 1 1

tabella di verità dello XOR

A B XOR
0 0 0
0 1 1
1 0 1
1 1 0

tabella di verità del NOT

A NOT
0 1
1 0

Esempio:

MOV AL, 11010111b   ; mette un valore binario in AL
AND AL, 11111110b    ; mette in AL il risultato dell'AND logico tra i bit di AL e 11111110

Nell'esempio, l'istruzione AND di questo tipo mette a 0 l'ultimo bit di AL e lascia invariati gli altri.

istruzioni aritmetiche permettono di effettuare le operazioni aritmetiche di base: addizione, sottrazione, moltiplicazione e divisione.
ADD destinazione, sorgente effettua la somma tra destinazione e sorgente e mette il risultato in destinazione
SUB destinazione, sorgente effettua la sottrazione tra destinazione e sorgente e mette il risultato in destinazione
dove sorgente può essere una costante o il riferimento ad una variabile o ad un registro, e destinazione può essere solo il riferimento ad una variabile o ad un registro.
MUL sorgente (moltiplicazione tra numeri interi) si comporta in due casi in base alla lunghezza di sorgente
se sorgente è di tipo BYTE: In AX troviamo il risultato della moltiplicazione tra sorgente e AL
se sorgente è di tipo WORD: In DX:AX troviamo il risultato della moltiplicazione tra sorgente e AX
Vediamo che se moltiplichiamo tra loro due valori a 8 bit, il risultato è calcolato su 16 bit, mentre se moltiplichiamo valori a 16 bit il risultato è calcolato su 32 bit (servono due registri per contenerlo).
IMUL sorgente opera in modo del tutto analogo a MUL, ma considera il bit più significativo degli operandi come il segno (per informazioni sul sistema binario in complemento a due consultare l'appendice sistemi di numerazione).
DIV divisore (divisione tra numeri interi) come MUL, si comporta differentemente in base alla lunghezza di sorgente
se divisore è di tipo BYTE: viene effettuata la divisione di AX per divisore, il quoziente viene posto in AL e il resto in AH.
se divisore è di tipo WORD: viene effettuata la divisione di DX:AX per divisore, il quozieente viene posto in AX e il resto in DX.
IDIV divisore opera come DIV, ma su numeri relativi (come IMUL).
sorgente e divisore devono essere riferimenti ad un registro o ad una variabile.
INC sorgente incrementa di 1 il valore contenuto in sorgente
DEC sorgente decrementa di 1 il valore contenuto in sorgente
dove sorgente deve essere il riferimento ad un registro o ad una variabile
.

strutture di controllo
Il processore esegue le istruzioni così come si presentano, una dopo l'altra. Tuttavia possiamo, attraverso particolari strutture, controllare il flusso esecutivo in base ad una determinata condizione. In questo modo possiamo creare strutture di semplice selezione o di tipo iterativo (cicli). Le istruzioni assembly che vengono utilizzate per questo scopo sono principalmente di due tipo: salto e confronto.
I salti possono essere incondizionati o condizionati.
JMP indirizzo di riferimento effettua un salto incondizionato. In genere indirizzo di riferimento è un'etichetta.
Esisitono diverse istruzioni per i salti condizionati:
 

istruzione descrizione
JA salta se CF=0 e ZF=0
JAE salta se CF=0
JB salta se CF=1
JBE salta se CF=1 e ZF=0
JC salta se CF=1
JCXZ salta se CX=0
JE salta se ZF=1
JG salta se ZF=0 e SF=OF
JGE salta se SF=0
JL salta se SF!=OF
JLE salta se ZF=1 o SF!=OF
JNA salta se CF=1 o ZF=0
JNAE salta se CF=1
JNB salta se CF=0
JNBE salta se CF=0 e ZF=0
JNC salta se CF=0
JNE salta se ZF=0
JNG salta se ZF=1o SF!=OF
JNGE salta se SF!=OF
JNL salta se SF=OF
JNLE salta se ZF=0 e SF=0
JNO salta se OF=0
JNP salta se PF=0
JNS salta se SF=0
JNZ salta se ZF=0
JO salta se OF=1
JP salta se PF=1
JPE salta se PF=1
JPO salta se PF=0
JS salta se SF=1
JZ salta se ZF=1

il simbolo "!=" ha valore "diverso da"

La condizione del salto è sempre dettata dai valori del registro dei flag. I flag più usati per i salti sono:
ZF (flag zero) indica se l'ultima istruzione ha generato come risultato 0
SF (flag segno) indica se l'ultima istruzione ha generato un risultato di segno negativo
OF (flag overflow) indica se l'ultima istruzione ha generato un overflow (con troncamento del bit più significativo del risultato)

La tabella, scritta in quel modo, è di difficile utilizzo. Tuttavia effettuare un salto condizionato diventa semplicissimo grazie all'istruzione CMP (compare = confronta).
CMP op1, op2
La CMP si comporta esattamente come l'istruzione SUB destinazione, sorgente con la differenza che il risultato va perso e i registri destinazione e sorgente (op1 e op2) rimangono intatti. Quello che viene cambiato è invece il registro dei flag: il base ai valori di alcuni flag si può dedurre se op1 e op2 sono uguali, se è maggiore il primo oppure il secondo.
Così diventa semplice effettuare un salto:

CMP AX,0000h    ; confronta il valore di AX con il valore 0 esadecimale
JE <etichetta>        ; salta se i valori sono uguali

Al posto di 0000h possiamo mettere un qualsiasi valore, ovviamente, in base alle nostre necessità. E anche la condizione può essere diversa:

CMP AL,0Ah        ; confronta il valore di AL con il valore 0A esadecimale (10 decimale)
JA <etichetta>        ; salta se il primo è maggiore

I nomi dei salti non sono casuali, e ricordarli non è difficile: JE = Jump if Equal (salta se uguale), JA e JB sono rispettivamente: Salta se è maggiore l'operando A (il primo) o quello B (il secondo). Essi possono in genere essere combinati: l'istruzione JAE effettua il salto se op1 è maggiore (JA) o uguale (JE) rispetto a op2. Inoltre i nomi dei salti cambiano se si tratta di numeri interi o frazionari, con segno o senza.

Ecco una tabella simile alla precedente, ma molto più leggibile e più facile da consultare. Conviene averla a portata di mano quando si scrive un programma ASM.

istruzione descrizione con op1 ed op2
JA salta se op1>op2 interi assoluti
JAE salta se op1=>op2 interi assoluti
JB salta se op1<op2 interi assoluti
JBE salta se op1=<op2 interi assoluti
JE salta se op1=op2  
JG salta se op1>op2 interi relativi
JGE salta se op1=>op2 interi relativi
JL salta se op1<op2 interi relativi
JLE salta se op1=<op2 interi relativi
JNA salta se op1=<op2 interi assoluti
JNAE salta se op1<op2 interi assoluti
JNB salta se op1=>op2 interi assoluti
JNBE salta se op1>op2 interi assoluti
JNE salta se op1!=op2  
JNG salta se op1<=op2 interi relativi
JNGE salta se op1<op2 interi relativi
JNL salta se op1=>op2 interi relativi
JNLE salta se op1>op2 interi relativi
JNZ salta se op1!=op2  
JZ salta se op1=op2  

come prima, il simbolo "!=" ha valore "diverso da"

I salti condizionati e non possono venire combinati per formare le principali strutture utilizzate nei programmi: selezione e iterazione.

la struttura di selezione permette, in base al verificarsi o no di una condizione, di scegliere tra due blocchi di istruzioni (uno può anche essere  vuoto), corrisponde alla if (condizione) { sequenza 1 } else { sequenza 2 } del linguaggio C.

se Condizione allora
                             sequenza 1
                       altrimenti
                                sequenza 2
fine selezione

Per il modo di operare del processore, è più corretta la seguente forma:

se Condizione allora
                               esegui la sequenza 1
                                salta la sequenza 2
                       altrimenti
                                salta la sequenza 1
                                esegui la sequenza 2
fine selezione

In assembly:

JCondizione etichetta
         ... sequenza 2 ...
            JMP fine_sel
etichetta:
         ... sequenza 1 ...
fine_sel:

In pratica, se la condizione espressa dall'istruzione di salto utilizzata è verificata, il programma salta sequenza2 ed esegue sequenza1, altrimenti continua con l'esecuzione di sequenza2 e salta sequenza1.
Vediamo un esempio:
Se AX>0 metti in AX il valore 0, altrimenti poni in AX il valore 0001h
in assembly:
 

            CMP AX,0000h           ;confronto tra AX e 0
           JA maggiore            ;salta se op1>op2
           MOV AX,0001h           ;mette in AX il valore 1
           JMP fine_sel           ;salta incondizionatamente all'etichetta fine_sel
maggiore:  MOV AX,0000h           ;mette in AX il valore 0
fine_sel:

Il ciclo a controllo in coda
L'iterazione è una struttura che permette di ripetere più volte un istruzione sotto il controllo di una condizione. In C non mi sembra esista una struttura di questo tipo. Se conoscete il Pascal, questa equivale alla Repeat <sequenza> Until condizione. In pratica ripete <sequenza> fino a quando condizione non si verifica (condizione = True).

ripeti
      istruzioni
finchè condizione

in assembly, attraverso la logica dei salti, viene rappresentato così:

inizio_ciclo:
    istruzioni
JNcondizione inizio_ciclo

esempio:

MOV AX, 0000h
inizio_ciclo:
    INC AX
    CMP AX, 000Ah    ;confronta AX e il valore  0Ah (10d)
JNE inizio_ciclo     ;salta all'inizio (e ripete il ciclo) se diverso

Dato che il controllo della condizione viene eseguito alla fine del ciclo, le istruzioni in sequenza vengono eseguite comunque almeno una volta, anche se la condizione era già verificata in partenza. In pratica:

MOV AX, 000Ah
inizio_ciclo:
        INC AX
        CMP AX, 000Ah
JNE inizio_ciclo

Questo spezzone di codice dovrebbe controllore se AX = 10d, e in caso contrario incrementare AX. In caso favorevole uscire dal ciclo. Vediamo però che AX vale già 10d, tuttavia tale registro viene comunque incrementato (alla fine varrà 000Bh). Inoltre, in questo particolare programma, il ciclo non finirà mai: AX varrà 11, poi 12, poi 13 e non diventerà mai uguale a 10. Sarebbe buona norma, nelle condizioni, evitare di esprimere un'uguaglianza:

MOV AX, 000Ah
inizio_ciclo:
        INC AX
        CMP AX, 000Ah
JB inizio_ciclo                     ; salta se minore (invece di salta se non uguale)

In questo modo abbiamo risolto il problema del ciclo infinito. Tuttavia, a causa del fatto che la sequenza viene eseguita almeno una volta, in genere si evita il ciclo a controllo in coda e si utilizza invece quello a controllo in testa.

Il ciclo a controllo in testa
Una struttura iterativa a controllo in testa si può descrivere, ad alto livello, così:

mentre condizione
    istruzioni
fine ciclo

Equivale alla while (condizione) { sequenza } del C.

in assembly:

inizio_ciclo:
    JNcondizione fine_ciclo
        sequenza
    JMP inizio_ciclo
fine_ciclo

esempio:

inizio_ciclo:
    CMP AX,0Ah            ;confronta AX con 10d
    JNE fine_ciclo        ;salta se diverso
        INC AX            ;incrementa AX
    JMP inizio_ciclo
fine_ciclo:

La differenza tra questa struttura e quella a controllo in coda sta nel fatto che se la condizione è inizialmente verificata, la sequenza di istruzioni non viene eseguita nemmeno una volta.

Il ciclo a contatore
Il ciclo a contatore ha una struttura di questo tipo:

ripeti per N volte
    sequenza
fine ciclo

Possiamo utilizzare un ciclo a contatore se vogliamo ripetere un blocco di istruzioni per un numero di volte noto a priori.
i cicli in assembly sono in genere a decremento:

CONTATORE = N
ripeti
    sequenza
    decrementa CONTATORE
finché CONTATORE = 0

Come contatore si usa di solito il registro CX (registro contatore, appunto), perchè esiste un'istruzione che esegue le ultime due istruzioni automaticamente: l'istruzione LOOP: decrementa CX e, se CX = 0, salta all'etichetta specificata.

Grazie all'istruzione LOOP diventa semplice scrivere un ciclo a contatore in assembly:

MOV CX, <N>            ; dove n è il numero di ripetizioni da eseguire
inizio_ciclo:
    sequenza
LOOP inizio_ciclo


l'input/output tramite l'INT 21h
L'assembly non prevede funzioni di input/output gà pronte. Il programmatore deve crearsi le proprie routine o appoggiarsi a quelle create da terze parti. Tra queste ultime troviamo quelle del DOS, accessibili richiamando l'interrupt 21h. Basta mettere il codice del servizio richiesto in AX ed usare l'istruzione INT 21h. Tra le funzioni più comuni per l'input/output da tastiera:

servizio 01h Aquisizione di un carattere da tastiera con eco sul video
Attende la pressione di un tasto, e restituisce il AL il codice ASCII del tasto premuto
servizio 07h Acquisizione di un carattere da tastiera senza eco sul video
Come il servizio 01h, ma non visualizza il carattere sullo schermo
servizio 02h visualizzazione di un carattere a video
Stampa il carattere il cui codice ASCII è contenuto in DL

Quindi, per acquisire un carattere (con eco sul video):

MOV AX,0001h    ; servizio 01h
INT 21h         ; se AX=0001h, allora in AL va il codice ACII del tasto premuto

E volendo poi stamparlo:

MOV DL,AL       ; copio il codice ASCII del tasto letto il DL
MOV AX,0002h    ; servizio 02h
INT 21h         ; se AX=0002h, allora stampa il carattere di codice ASCII in DL


Vediamo che sia le operazioni di acquisizione che di stampa fanno rifementi ai codici di carattere ASCII. Nel caso si voglia leggere in input una cifra numerica, per risalire al valore numerico basta sottrarre il valore 40h al suo codice ASCII. Infatti 40h in ASCII corrisponde al carattere "0", 41h al "1" e così via...
In assembly, per stampare un solo carattere sfruttando i servizi DOS, ci vogliono tre righe di programma! Per questo l'interfaccia di un applicazione si scrive con un linguaggio ad alto livello. Più avanti vedremo come scrivere programmi utilizzando sia l'assembly sia il C.

le stringhe in assembly

la pila o stack Lo stack è un'area di memoria in cui è possibile inserire un elemento con l'istruzione PUSH o estrarne uno con la POP. La particolarità dello stack è che esso è una struttura di tipo LIFO (Last In First Out): in pratica l'istruzione POP estrae l'ultimo elemento inserito tramite la PUSH. Inoltre, ogni elemento della pila è di tipo word. Spieghiamolo con un esempio: una volta eseguite le istruzioni

PUSH 0001h
PUSH 0002h
PUSH 0003h

la pila, inizialmente vuota, conterrà 3 elementi (quindi 3 word = 6 byte). Estraendo i valori:

POP AX    ; in AX 0003h
POP BX    ; in BX 0002h
POP CX    ; in CX 0001h

i valori di AX, BX e CX saranno rispettivamente 0003h, 0002h e 0001h. Questo perchè l'istruzione POP AX ha estratto l'ultimo elemento inserito nella pila, cioè 0003h.

Abbiamo visto come vengono utilizzati due dei registri puntatori del processore (BP e SP), servono ad indicare i limiti in memoria entro i quali sono contenuti i dati inseriti nello stack. Risulta chiaro ora anche il significato della parola stack nella intestazione del segmento sistema (struttura di un listato assembly), in pratica indichiamo che quello è il segmento di memoria che dovrà ospitare fisicamente i dati dello stack.
Lo stack viene utilizzato soprattutto per il salvataggio temporaneo del valore di qualche registro che serve per altri scopi:

PUSH DX             ; salva il registro DX
PUSH AX             ; salva il registro AX
MOV DL, 'c'         ; mette in DL il codice ASCII del carattere 'c' (con il compilatore Borland TASM, con gli altri non so...)
MOV AX, 0002h  ; servizio 02h: stampa di un carattere a video
INT 21h                 ; stampa il carattere 'c'
POP AX                ; ripristina il registro AX
POP DX                ; ripristina il registro DX

Inoltre, è utilizzato anche per le chiamate alle procedure.

le procedure assembly
In una procedure possiamo scrivere il codice per eseguire operazioni che verranno utilizzate molte volte dal programma. Ad esempio, possiamo scrivere una procedura che legga un numero a più cifre dalla tastiera.
Per includere un sottoprogramma in un listato di codice assembly, dobbiamo metterlo tra le parole PROC ed ENDP. Inoltre il tutto va sistemato di seguito al programma principale. Vediamo come cambia la struttura di un programma con l'utilizzo di una procedura:

;nome del programma
;funzioni del programma

Dati SEGMENT
Dati ENDS

Sistema SEGMENT STACK
Sistema ENDS

Codice SEGMENT
        ASSUME CS:Codice, DS:Dati, SS:Sistema, ES:Dati
        inizio: <...>
                <...>
       
        Nome_proc PROC
                <...>
                <...>

      Nome_proc ENDP

Codice ENDS
END inizio

Nome_proc è il nome della funzione, ad esempio LeggiNumero.
Per richiamare una procedura si utilizza l'istruzione CALL nome_procedura. L'istruzione CALL si comporta come l'istruzione JMP, cioè salta alla parte di codice relativa alla procedura, ma prima di farlo mette nello stack il valore corrente del registro IP. In questo modo, alla fine della procedura tale valore può essere ripristinato con l'istruzione RET e l'esecuzione può continuare dal punto in cui era stata interrotta (per eseguire la procedura). Vediamo un esempio:

;nome del programma
;funzioni del programma

Dati SEGMENT
Dati ENDS

Sistema SEGMENT STACK
Sistema ENDS

Codice SEGMENT
        ASSUME CS:Codice, DS:Dati, SS:Sistema, ES:Dati
        inizio: <...>
                CALL print_char

                 <...>

        print_char PROC
               
MOV DL,0040h
            MOV AX,0002h
               
INT 21h
                 RET
        print_char ENDP

Codice ENDS
END inizio

La procedura print_char stampa un carattere (nell'esempio il carattere "0") sullo schermo. Tuttavia, il contenuto del registro AX viene perso. Per ovviare a questo, si salva e successivamente ripristina il suo valore con l'aiuto dello stack:

print_char PROC
    PUSH AX        ; salva il valore di AX nello stack
    MOV DL,0040h
    MOV AX,0002h
    INT 21h
    POP AX         ; ripristina il valore di AX
    RET
print_char ENDP

Bisogna fare attenzione alle istruzioni che operano sullo stack all'interno di un procedura: nello stack è infatti conservato l'indirizzo di partenza. Dobbiamo sempre effettuare tante PUSH quante POP, altrimenti l'istruzione RET prenderebbe dallo stack un valore diverso da quello cercato e il programma rischia un crash.

Una procedura di questo tipo comunque non ha motivo di essere. Più utile sarebbe una procedura che stampi un carattere qualsiasi, specificato dal programma chiamate. Questo porta un problema: come passare dei parametri alla procedura? Esistono due modi: attraverso i registri o attraverso lo stack. Il modo più semplice è quello di utilizzare dei registri:

;nome del programma
;funzioni del programma

Dati SEGMENT
Dati ENDS

Sistema SEGMENT STACK
Sistema ENDS

Codice SEGMENT
ASSUME CS:Codice, DS:Dati, SS:Sistema, ES:Dati
inizio: <...>
        MOV DL,0040h
        CALL print_char

        <...>

print_char PROC
    PUSH AX
    MOV AX,0002h
    INT 21h
    POP AX
    RET
print_char ENDP

Codice ENDS
END inizio

In questo caso il carattere da stampare è stato specificato in un registro dal programma principale. Dato che la funzione di stampa di un carattere da parte dell'Interrupt 21h prende il codice ASCII in DL, è stato utilizzato quel registro per passare il parametro, evitando poi di doverlo spostare all'interno della procedura.

Il metodo più utilizzato però è quello che impega lo stack, che anche se leggermente più complesso lascia maggiore libertà al programmatore. Consiste nel mettere nello stack i parametri da passare prima di richiamare la procedura. Questa poi, come prima operazione, provvederà al loro recupero. La complessità deriva dal fatto che lo stack è utilizzato anche dal sistema per conservare l'indirizzo di ritorno della procedura. La procedura, per accedere ai dati nello stack, potrà utilizzare il puntatore BP (vedi: la cpu).

Per un elenco più completo delle istruzioni assembly, controllate l'appendice.


torna alla homepage / torna all'inizio / prossima sezione